<?php
/**
 * Project: AIGC
 * Author: moufer <moufer@163.com>
 * Date: 2024/10/13 22:48
 */

namespace module\ai\library\channel\chat;

use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\GuzzleException;
use JsonException;
use module\ai\library\channel\abstract\Chat;
use module\ai\library\channel\exception\ChatException;
use module\ai\library\channel\interface\ChatInterface;
use module\ai\library\OpenAI\Factory;
use mof\exception\LogicException;
use OpenAI\Enums\Transporter\ContentType;
use OpenAI\Exceptions\ErrorException;
use OpenAI\Exceptions\UnserializableResponse;
use Psr\Http\Message\ResponseInterface;
use think\facade\Cache;

class BaiduChat extends Chat
{
    protected string $baseUrl = 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1';

    public function withSystem(string $system): ChatInterface
    {
        $this->payload['system'] = $system;
        return $this;
    }

    protected function chat(): \OpenAI\Resources\Chat|\module\ai\library\OpenAI\Chat
    {
        if (!$this->client) {
            $this->client = (new Factory())
                ->withBaseUri($this->baseUrl)
                ->withQueryParam('access_token', $this->getAccessToken())
                ->make();
        }

        $chat = $this->client->chat();
        $chat->getTransporter()->setThrowIfJsonErrorHandler(
            function (ResponseInterface $response, string|ResponseInterface $contents) {
                $this->checkErrorData($response, $contents);
            }
        );

        return $chat->withApiPath($this->getEndPoint())
            ->withFormatData(fn($data) => $this->formattingData($data));
    }

    /**
     * 获取AccessToken
     * @return string
     */
    protected function getAccessToken(): string
    {
        //从缓存里获取去
        $clientId = $this->keyConfig['api_key'];
        $clientSecret = $this->keyConfig['secret_key'];

        $cacheKey = "ai_access_token:baidu:$clientId";
        $token = Cache::get($cacheKey);
        if (!$token || $token['expire_time']) {
            $url = 'https://aip.baidubce.com/oauth/2.0/token';
            $params = [
                'grant_type'    => 'client_credentials',
                'client_id'     => $clientId,
                'client_secret' => $clientSecret,
            ];
            $client = new Client();
            try {
                $response = $client->post($url, [
                    'form_params' => $params,
                    'headers'     => [
                        'Content-Type' => 'application/json',
                    ]
                ]);
                if ($response->getStatusCode() == 200) {
                    $data = json_decode($response->getBody()->getContents(), true);
                    if (isset($data['error'])) {
                        throw new LogicException(
                            sprintf('获取AccessToken失败:%s(%s)', $data['error_description'], $data['error'])
                        );
                    }
                    $token = [
                        'access_token' => $data['access_token'],
                        'expire_time'  => time() + $data['expires_in'],
                    ];
                    Cache::set($cacheKey, $token, $data['expires_in']);
                } else {
                    throw new LogicException(
                        sprintf('获取AccessToken失败:%s', $response->getStatusCode())
                    );
                }
            } catch (ClientException $e) {
                $data = json_decode($e->getResponse()->getBody()->getContents(), true);
                throw new LogicException($e->getMessage(), $e->getMessage());
            } catch (GuzzleException $e) {
                throw new LogicException($e->getMessage(), $e->getMessage());
            }
        }

        return $token['access_token'];
    }

    /**
     * 获取模型地址
     * @return string|null
     */
    protected function getEndPoint(): ?string
    {
        $model = $this->payload['model'] ?? '';
        if (!$model) {
            throw new ChatException('未指定文心一言模型');
        }
        return "wenxinworkshop/chat/{$model}";
    }

    /**
     */
    protected function formattingData(array $data): array
    {
        $data['model'] = $this->payload['model'];
        if (isset($data['is_end'])) {
            //流式
            $data['choices'] = [
                [
                    'index'         => $data['sentence_id'] ?? 0,
                    'delta'         => [
                        'role'    => 'assistant',
                        'content' => $data['result']
                    ],
                    'finish_reason' => $data['is_end'] ? 'stop' : ''
                ]
            ];
        } else {
            $data['choices'] = [
                [
                    'index'         => 0,
                    'message'       => [
                        'role'    => 'assistant',
                        'content' => $data['result']
                    ],
                    'finish_reason' => 'stop'
                ]
            ];
        }
        return $data;
    }

    /**
     * @throws ErrorException
     * @throws UnserializableResponse
     */
    protected function checkErrorData($response, $contents): void
    {
        if (!str_contains($response->getHeaderLine('Content-Type'), ContentType::JSON->value)) {
            return;
        }

        $limitInfo = array_filter(array_keys($response->getHeaders()), function ($key) {
            return str_starts_with($key, 'X-Ratelimit-');
        });

        if (!empty($limitInfo)) return;

        if ($contents instanceof ResponseInterface) {
            $contents = $contents->getBody()->getContents();
        }

        try {
            /** @var array{error?: array{message: string|array<int, string>, type: string, code: string}} $response */
            $response = json_decode($contents, true, flags: JSON_THROW_ON_ERROR);

            if (isset($response['error_code'])) {
                throw new ErrorException(['message' => $response['error_msg'], 'code' => $response['error_code']]);
            }
        } catch (JsonException $jsonException) {
            throw new UnserializableResponse($jsonException);
        }

    }

}